﻿using System;
using System.Globalization;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.ComponentModel;

namespace HeroldIT.Dnn.JQuery.UI
{
    /// <summary>
    /// Creates a jQuery UI Tabs widget.
    /// </summary>
    [ParseChildren(true)]
    public class Tabs : MultiView
    {
        #region constants

        private static readonly Regex safeIdRegex = new Regex("\\W+", RegexOptions.Compiled | RegexOptions.CultureInvariant);

        #endregion

        #region static methods

        private static string GenerateSafeId(string title)
        {
            return safeIdRegex.Replace(title, "-");
        }

        #endregion

        #region fields

        private bool loadingControlState;

        private int loadedActiveViewIndex = -1;

        private bool activeViewIndexSet;

        #endregion

        #region properties

        /// <summary>
        /// Whether to initialize the widget using an inline block instead of the document.ready event.
        /// </summary>
        [DefaultValue(false)]
        public bool UseInlineBlockInitialization { get; set; }

        /// <summary>
        /// Gets or sets the name of the theme to use for the control.
        /// </summary>
        public string ThemeName { get; set; }

        /// <summary>
        /// Gets the collection of <see cref="TabPane"/>s to show in the <see cref="Tabs"/> control.
        /// </summary>
        public TabPaneCollection TabPanes
        {
            get
            {
                return (TabPaneCollection) this.Controls;
            }
        }

        /// <summary>
        /// Gets or sets the index of the active <see cref="T:System.Web.UI.WebControls.View"/> control within a <see cref="T:System.Web.UI.WebControls.MultiView"/> control.
        /// </summary>
        /// <returns>
        /// The zero-based index of the active <see cref="T:System.Web.UI.WebControls.View"/> control within a <see cref="T:System.Web.UI.WebControls.MultiView"/> control. The default is -1, indicating that no view is set as active.
        /// </returns>
        /// <exception cref="T:System.ArgumentOutOfRangeException">The specified index was set to less than -1, or greater than or equal to the number of items on the list. </exception>
        public override int ActiveViewIndex
        {
            get
            {
                if (this.loadedActiveViewIndex >= 0 && !this.activeViewIndexSet)
                {
                    return this.loadedActiveViewIndex;
                }
                return base.ActiveViewIndex;
            }
            set
            {
                base.ActiveViewIndex = value;
                if (!this.loadingControlState)
                {
                    this.activeViewIndexSet = true;
                }
            }
        }

        /// <summary>
        /// Gets the name for the active tab hidden field.
        /// </summary>
        private string ActiveTabHiddenFieldName
        {
            get { return this.ClientID + this.ClientIDSeparator + "activeTab"; }
        }

        #endregion

        #region jQuery UI settings properties

        private Dictionary<string, object> ajaxOptions;

        /// <summary>
        /// Gets or sets additional Ajax options to consider when loading tab content (see $.ajax).
        /// </summary>
        [Setting("ajaxOptions", DictionarySerializationMode = DictionarySerializationMode.Normal)]
        public Dictionary<string, object> AjaxOptions
        {
            get
            {
                if (null == this.ajaxOptions)
                {
                    this.ajaxOptions = new Dictionary<string, object>();
                }
                return this.ajaxOptions;
            }
        }

        /// <summary>
        /// Gets or sets whether or not to cache remote tabs content, e.g. load only once
        /// or with every click. Cached content is being lazy loaded, e.g. once and only
        /// once for the first click. Note that to prevent the actual Ajax requests from
        /// being cached by the browser you need to provide an extra cache: false flag to
        /// ajaxOptions.
        /// </summary>
        [Setting("cache")]
        [DefaultValue(false)]
        public bool Cache { get; set; }

        /// <summary>
        /// Gets or sets whether to allow an already selected tab to become unselected
        /// again upon reselection.
        /// </summary>
        [Setting("collapsible")]
        [DefaultValue(false)]
        public bool Collapsible { get; set; }

        private Dictionary<string, object> cookie;

        /// <summary>
        /// Gets or sets how to store the latest selected tab in a cookie. The cookie is
        /// then used to determine the initially selected tab if the selected option is
        /// not defined. Requires cookie plugin. The object needs to have key/value pairs
        /// of the form the cookie plugin expects as options. Available options (example):
        /// { expires: 7, path: '/', domain: 'jquery.com', secure: true }. Since jQuery UI
        /// 1.7 it is also possible to define the cookie name being used via name property.
        /// </summary>
        /// <remarks>
        /// This setting will be converted to a better .NET representation in future versions
        /// of the library. The Dictionary is currently used due to lack of a custom Cookie
        /// object.
        /// </remarks>
        [Setting("cookie", DictionarySerializationMode = DictionarySerializationMode.Normal)]
        public Dictionary<string, object> Cookie
        {
            get
            {
                if (null == this.cookie)
                {
                    this.cookie = new Dictionary<string, object>();
                }
                return this.cookie;
            }
        }

        /// <summary>
        /// Gets or sets whether to allow an already selected tab to become unselected
        /// again upon reselection.
        /// </summary>
        [Obsolete("Obsolete in jQuery 1.7. Use the Collapsible property instead.")]
        [Setting("deselectable")]
        [DefaultValue(false)]
        public bool Deselectable { get; set; }

        private string @event = "click";

        /// <summary>
        /// Gets or sets the type of event to be used for selecting a tab.
        /// </summary>
        /// <remarks>
        /// This property may be reworked to use an enum in a future version.
        /// </remarks>
        [Setting("event")]
        [DefaultValue("click")]
        public string Event
        {
            get { return this.@event; }
            set { this.@event = value; }
        }

        private Dictionary<string, object> fx;

        /// <summary>
        /// Gets the defintion for animations for hiding and showing tab panels. The duration
        /// option can be a string representing one of the three predefined speeds ("slow",
        /// "normal", "fast") or the duration in milliseconds to run an animation (default is
        /// "normal").
        /// </summary>
        [Setting("fx", DictionarySerializationMode = DictionarySerializationMode.Normal)]
        public Dictionary<string, object> Fx
        {
            get
            {
                if (null == this.fx)
                {
                    this.fx = new Dictionary<string, object>();
                }
                return this.fx;
            }
        }

        private string idPrefix = "ui-tabs-";

        /// <summary>
        /// Gets or sets a prefix for remote tabs, if their anchor elements have no title
        /// attribute, to generate an id from. an id/fragment identifier is created from
        /// this prefix and a unique id returned by $.data(el), for example "ui-tabs-54".
        /// </summary>
        [Setting("idPrefix")]
        [DefaultValue("ui-tabs-")]
        public string IdPrefix
        {
            get { return this.idPrefix; }
            set { this.idPrefix = value; }
        }

        private string panelTemplate = "<div></div>";

        /// <summary>
        /// Gets or sets an HTML template from which a new tab panel is created in case
        /// of adding a tab with the add method or when creating a panel for a remote
        /// tab on the fly.
        /// </summary>
        [Setting("panelTemplate")]
        [DefaultValue("<div></div>")]
        public string PanelTemplate
        {
            get { return this.panelTemplate; }
            set { this.panelTemplate = value; }
        }

        private string spinner = "<em>Loading&#8230;</em>";

        /// <summary>
        /// Gets or set the HTML content of this string is shown in a tab title while remote
        /// content is loading. Pass in empty string to deactivate that behavior.
        /// </summary>
        [Setting("spinner", EmptyValueSerializationMode = EmptyValueSerializationMode.Emit)]
        [DefaultValue("<em>Loading&#8230;</em>")]
        public string Spinner
        {
            get { return this.spinner; }
            set { this.spinner = value; }
        }

        private string tabTemplate = "<li><a href=\"#{href}\"><span>#{label}</span></a></li>";

        /// <summary>
        /// Gets or sets an HTML template from which a new tab is created and added. The
        /// placeholders #{href} and #{label} are replaced with the url and tab label that
        /// are passed as arguments to the add method.
        /// </summary>
        [Setting("tabTemplate")]
        [DefaultValue("<li><a href=\"#{href}\"><span>#{label}</span></a></li>")]
        public string TabTemplate
        {
            get { return this.tabTemplate; }
            set { this.tabTemplate = value; }
        }

        #endregion

        #region jQuery client events

        /// <summary>
        /// Defines the client events for the <see cref="Tabs"/> control.
        /// </summary>
        public sealed class ClientEventDefinitions
        {
            #region properties

            [Setting("select")]
            internal ClientEventHandler SelectHandler
            {
                get { return this.Select == null ? null : new ClientEventHandler("event,ui", this.Select); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when clicking a tab.
            /// The names of the arguments to the function are <c>event</c> and <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Select { get; set; }

            [Setting("load")]
            internal ClientEventHandler LoadHandler
            {
                get { return this.Load == null ? null : new ClientEventHandler("event,ui", this.Load); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered after the content of
            /// a remote tab has been loaded. The names of the arguments to the function
            /// are <c>event</c> and <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Load { get; set; }

            [Setting("show")]
            internal ClientEventHandler ShowHandler
            {
                get { return this.Show == null ? null : new ClientEventHandler("event,ui", this.Show); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when a tab is shown.
            /// The names of the arguments to the function are <c>event</c> and <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Show { get; set; }

            [Setting("add")]
            internal ClientEventHandler AddHandler
            {
                get { return this.Add == null ? null : new ClientEventHandler("event,ui", this.Add); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when a tab is added.
            /// The names of the arguments to the function are <c>event</c> and <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Add { get; set; }

            [Setting("remove")]
            internal ClientEventHandler RemoveHandler
            {
                get { return this.Remove == null ? null : new ClientEventHandler("event,ui", this.Remove); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when a tab is removed.
            /// The names of the arguments to the function are <c>event</c> and <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Remove { get; set; }

            [Setting("enabled")]
            internal ClientEventHandler EnabledHandler
            {
                get { return this.Enabled == null ? null : new ClientEventHandler("event,ui", this.Enabled); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when a tab is enabled.
            /// The names of the arguments to the function are <c>event</c> and <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Enabled { get; set; }

            [Setting("disabled")]
            internal ClientEventHandler DisabledHandler
            {
                get { return this.Disabled == null ? null : new ClientEventHandler("event,ui", this.Disabled); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when a tab is disabled.
            /// The names of the arguments to the function are <c>event</c> and <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Disabled { get; set; }

            #endregion

            #region constructors

            /// <summary>
            /// Creates a new <see cref="ClientEventDefinitions"/> instance.
            /// </summary>
            internal ClientEventDefinitions()
            {
            }

            #endregion
        }

        private readonly ClientEventDefinitions clientEvents = new ClientEventDefinitions();

        /// <summary>
        /// Holds the definitions for the client events.
        /// </summary>
        [SettingGroup]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public ClientEventDefinitions ClientEvents
        {
            get
            {
                return this.clientEvents;
            }
        }

        #endregion

        #region events

        /// <summary>
        /// This event is raised, just before the control applies its javascript
        /// settings to the initialization code. You can use it to manipulate the
        /// settings dictionary.
        /// </summary>
        /// <remarks>
        /// Be careful when using this event. It allows you to reach deeply into
        /// the internal workings of the control, possibly breaking functionality.
        /// </remarks>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public event EventHandler<SettingsEventArgs> ApplyingSettings;

        /// <summary>
        /// Fires the <see cref="ApplyingSettings"/> event.
        /// </summary>
        /// <param name="e">The <see cref="SettingsEventArgs"/> passed to the event.</param>
        protected virtual void OnApplyingSettings(SettingsEventArgs e)
        {
            if (null != this.ApplyingSettings)
            {
                this.ApplyingSettings(this, e);
            }
        }

        #endregion

        #region page lifecycle

        /// <summary>
        /// Creates a <see cref="TabPaneCollection"/> to hold the child controls of the
        /// <see cref="Tabs"/> control.
        /// </summary>
        /// <returns>A <see cref="TabPaneCollection"/> to contain the <see cref="TabPane"/>
        /// controls of the current <see cref="Tabs"/> control.</returns>
        protected override ControlCollection CreateControlCollection()
        {
            return new TabPaneCollection(this);
        }

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            this.Page.RegisterJQueryUI();

            base.OnInit(e);

            string activeTab = this.Page.Request.Params[this.ActiveTabHiddenFieldName];
            if (!String.IsNullOrEmpty(activeTab))
            {
                int activeTabIndex;
                if (int.TryParse(activeTab, NumberStyles.Integer, CultureInfo.InvariantCulture, out activeTabIndex))
                {
                    this.loadedActiveViewIndex = activeTabIndex;
                }
            }
        }

        /// <summary>
        /// Loads the current state of the <see cref="T:System.Web.UI.WebControls.MultiView"/> control.
        /// </summary>
        /// <param name="state">An <see cref="T:System.Object"/> that represents the state of the <see cref="T:System.Web.UI.WebControls.MultiView"/> control.</param>
        protected override void LoadControlState(object state)
        {
            this.loadingControlState = true;

            base.LoadControlState(state);

            this.loadingControlState = false;
        }

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            if (this.Visible && !this.UseInlineBlockInitialization)
            {
                string settingsJson = this.GetSettingsJson();
                this.Page.RegisterJQueryOnDocumentReadyBlock(String.Format(
                        CultureInfo.InvariantCulture,
                        "$('#{0}').tabs({1})",
                        this.ClientID,
                        settingsJson
                        ));
            }

            this.Page.ClientScript.RegisterHiddenField(this.ActiveTabHiddenFieldName, this.ActiveViewIndex.ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Writes the <see cref="Tabs"/> control content to the specified <see cref="T:System.Web.UI.HtmlTextWriter"/> object, for display on the client. 
        /// </summary>
        /// <param name="writer">An <see cref="T:System.Web.UI.HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
        protected override void Render(HtmlTextWriter writer)
        {
            string themeName = this.ThemeName;
            if (String.IsNullOrEmpty(themeName))
            {
                themeName = this.Page.GetDefaultJQueryUITheme();
            }
            if (!String.IsNullOrEmpty(themeName))
            {
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "jquery-ui-" + themeName);
                writer.RenderBeginTag(HtmlTextWriterTag.Div);
            }

            // render container with id
            writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
            writer.RenderBeginTag(HtmlTextWriterTag.Div);

            // render ul for tabs
            writer.RenderBeginTag(HtmlTextWriterTag.Ul);

            for (int i = 0; i < this.TabPanes.Count; i++)
            {
                TabPane pane = this.TabPanes[i];
                writer.RenderBeginTag(HtmlTextWriterTag.Li);

                string title = pane.Title;
                string safeId;
                if (String.IsNullOrEmpty(title))
                {
                    title = "Tab " + i;
                    safeId = this.ClientID + "-tab-" + i;
                }
                else
                {
                    safeId = this.ClientID + "-" + GenerateSafeId(title);
                }
                writer.AddAttribute(HtmlTextWriterAttribute.Href, "#" + safeId);
                writer.RenderBeginTag(HtmlTextWriterTag.A);
                writer.WriteEncodedText(title);
                writer.RenderEndTag();
                writer.RenderEndTag();
            }

            // end render ul for tabs
            writer.RenderEndTag();

            // render containers
            for (int i = 0; i < this.TabPanes.Count; i++)
            {
                TabPane pane = this.TabPanes[i];
                string title = pane.Title;
                string safeId;
                if (String.IsNullOrEmpty(title))
                {
                    safeId = this.ClientID + "-tab-" + i;
                }
                else
                {
                    safeId = this.ClientID + "-" + GenerateSafeId(title);
                }
                writer.AddAttribute(HtmlTextWriterAttribute.Id, safeId);
                writer.RenderBeginTag(HtmlTextWriterTag.Div);
                pane.RenderControl(writer);
                writer.RenderEndTag();
            }

            // end render container with id
            writer.RenderEndTag();

            if (!String.IsNullOrEmpty(themeName))
            {
                writer.RenderEndTag();
            }

            if (this.UseInlineBlockInitialization)
            {
                string settingsJson = this.GetSettingsJson();
                writer.Write("<script type=\"text/javascript\">//<![CDATA[\njQuery('#{0}').tabs({1});\n//]]></script>",
                             this.ClientID,
                             settingsJson);
            }
        }

        #endregion

        #region methods

        /// <summary>
        /// Gets the serialized settings for this <see cref="Tabs"/> widget.
        /// </summary>
        /// <returns>The serialized settings for this <see cref="Tabs"/> widget.</returns>
        private string GetSettingsJson()
        {
            Dictionary<string, object> settings = SettingSerializer.FetchSettings(this);
            int selected = Math.Min(this.Views.Count - 1, Math.Max(0, this.ActiveViewIndex));
            if (selected > 0)
                settings["selected"] = selected;
            List<int> disabled = new List<int>();
            for (int i = 0; i < this.TabPanes.Count; i++)
            {
                TabPane tabPane = this.TabPanes[i];
                if (tabPane.Disabled)
                    disabled.Add(i);
            }
            if (disabled.Count > 0)
            {
                settings["disabled"] = disabled;
            }
            settings["select"] = new ClientEventHandler(
                this.ClientEvents.SelectHandler ?? new ClientEventHandler("event,ui", ""),
                String.Format(CultureInfo.InvariantCulture,
                              "jQuery('#{0}').val(ui.index);",
                              this.ActiveTabHiddenFieldName));
            this.OnApplyingSettings(new SettingsEventArgs(settings));
            return SettingSerializer.Serialize(settings);
        }

        #endregion
    }
}
